package me.w70.fps.render;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Transparency;
import java.awt.image.BufferedImage;

import java.util.Hashtable;
import java.util.List;

import me.w70.fps.FPS;
import me.w70.fps.actor.Player;
import me.w70.fps.constants.Angles;
import me.w70.fps.map.Cell;
import me.w70.fps.map.Map;



public class GameWindow {
	private boolean useimages;
	private boolean useimagesfloor = true;
	private boolean useimagesceiling = true;
	private boolean variablelightlevels = false;
	private boolean variablelightfloors = false;
	private boolean variablelightceilings = false;
	private final int WALL_HEIGHT = 64;
	private FPS appletptr;
	protected int width;
	protected int height;
	private Player player;
	private Angles angles;
	private Map map;
	protected Image offscreengraphics;
	protected Graphics appletgraphics;
	// Pre-calculated tables of values for rendering.
	public float[] fishbowleffecttable;
	
	// Image loader
	private MediaTracker imagecache;
	// Texture data
	// Commented-out ones not used by the test map so not needed yet.
	private ImageData bluestone = new ImageData();
	private ImageData bluestonecell = new ImageData();
	private ImageData bluestonecellskeleton = new ImageData();
	//private ImageData colorstone = new ImageData();
	private ImageData door = new ImageData();
	private ImageData door2 = new ImageData();
	//private ImageData eagle = new ImageData();
	private ImageData greystone = new ImageData();
	private ImageData greystonehitler = new ImageData();
	private ImageData greystoneeagle = new ImageData();
	private ImageData greystonebanner = new ImageData();
	//private ImageData redbrick = new ImageData();
	private ImageData wood = new ImageData();
	private ImageData woodhitler = new ImageData();
	private ImageData woodeagle = new ImageData();
	//private ImageData purplestone = new ImageData();
	private ImageData elevatorswitch = new ImageData();
	private ImageData floortile = new ImageData();
	
	private ImageData floorimg = this.floortile;
	private ImageData ceilingimg = this.bluestone;
	
	// Map char to wall texture definitions
	protected Hashtable<String, ImageData> maptowalldefinitions;
	
	// RGB values for the non-textured screens.
	protected int[] ceilingcolor = {0, 0, 0};
	protected int[] floorcolor = {0, 0, 0};
	protected int[] scorescreencolor = {0, 64, 64};
	
	// Setup all the variables, image data, etc needed for the applet.
	public GameWindow(int width, int height, Image offscreengraphicscopy, Graphics appletgraphicscopy, Angles angle, FPS applet, Map map, boolean useimages, boolean hardcodedimages) {
		this.offscreengraphics = offscreengraphicscopy;
		this.appletgraphics = appletgraphicscopy;
		this.angles = angle;
		this.fishbowleffecttable = new float[this.angles.ANGLE60 + 1];
		this.appletptr = applet;
		this.map = map;
		this.width = width;
		this.height = height;
		this.useimages = useimages;
		if (useimages) {
			this.imagecache = new MediaTracker(this.appletptr);
			this.maptowalldefinitions = new Hashtable<String, ImageData>();
			if (hardcodedimages) {
				HardcodedImageData d = new HardcodedImageData();
				addHardcoded("1",  this.greystone, d.GREYSTONE_WIDTH, d.GREYSTONE_HEIGHT, d.GREYSTONE_DATA);
				addHardcoded("2",  this.bluestone, d.BLUESTONE_WIDTH, d.BLUESTONE_HEIGHT, d.BLUESTONE_DATA);
			}
			else {
				addImage("1", "greystone.png", this.greystone);
				addImage("2", "bluestone.png", this.bluestone);
				addImage("3", "bluestone-cell.png", this.bluestonecell);
				addImage("4", "bluestone-cellskeleton.png", this.bluestonecellskeleton);
				addImage("5", "greystone-hitler.png", this.greystonehitler);
				addImage("6", "greystone-eagle.png", this.greystoneeagle);
				addImage("7", "greystone-banner.png", this.greystonebanner);
				addImage("8", "wood.png", this.wood);
				addImage("9", "wood-eagle.png", this.woodeagle);
				addImage("A", "wood-hitler.png", this.woodhitler);
				addImage("E", "elevatorswitch.png", this.elevatorswitch);
				addImage("F", "greystone.png", this.floortile);
				addImage("|1", "door.png", this.door);
				addImage("|2", "door2.png", this.door2);
			}
		}
		else {
			this.useimagesfloor = false;
			this.useimagesceiling = false;
		}
	}
	
	// Convert an Image to an ImageData for rendering.
	public void addImage(String mapstring, String filename, ImageData imgptr) {
		// Fetch image from applet
		Image temp = appletptr.getImage(appletptr.getCodeBase(), "img/" + filename);
		// Wait for image to be loaded.
        imagecache.addImage(temp, 0);
        try {
			imagecache.waitForID(0);
		} 
        catch (InterruptedException e) {
			e.printStackTrace();
		}
        // Convert to Buffered Image, and then to ImageData
        BufferedImage bimgptr = convert(temp);
        imgptr.convertfromimage(bimgptr);
        // Finally, add it to our mappings.
        maptowalldefinitions.put(mapstring, imgptr);
	}
	
	// No longer needed, but keeping just in case.
	// Convert a hard-coded image's data into an ImageData object for rendering.
	public void addHardcoded(String mapstring, ImageData imgptr, int w, int h, int[][] data) {
		imgptr.feedHardcodedData(w, h, data);
		maptowalldefinitions.put(mapstring, imgptr);
	}
	
	// Converts an Image to a BufferedImage.
    public BufferedImage convert(Image image) {
    	// Why'd you feed it a BufferedImage?
        if (image instanceof BufferedImage) {
            return ((BufferedImage) image);
        }
    
        BufferedImage bufferedimage = null;
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        try {
            // Try to create the buffered image using their graphics device (faster)
            GraphicsDevice gs = ge.getDefaultScreenDevice();
            GraphicsConfiguration gc = gs.getDefaultConfiguration();
            bufferedimage = gc.createCompatibleImage(image.getWidth(null), image.getHeight(null), Transparency.OPAQUE);
        } 
        catch (HeadlessException e) {
            // No detected graphics device, so we need to create it ourselves using the RGB color model
            bufferedimage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_RGB);
        }
    
        // Grab the BufferedImage's graphics object...
        Graphics g = bufferedimage.createGraphics();
    
        // And paste onto it.
        g.drawImage(image, 0, 0, null);
        g.dispose();
    
        return bufferedimage;
    }

    // Fetch the Player object for drawing purposes.
	public void passPlayer(Player p) {
		this.player = p;
	}
    
	// Tell the applet to repaint itself.
	public void repaint() {
		appletptr.repaint();
	}
	
	// Draw an image scaled on the screen at x and y
	public void drawSpriteScaled(ImageData s, int x, int y, int w, int h) {
		for (int imgx = 0; imgx < s.width; imgx++) {
			for (int imgy = 0; imgy < s.height; imgy++) {
				// Scale X
				int dx = ((imgx - x) * 256) - (s.width * 128) + (w * 128);
				int texX = ((dx * 64) / w) / 256;
				// Scale Y
				int dy = ((imgy - y) * 256) - (s.height * 128) + (h * 128);    
				int texY = ((dy * 64) / h) / 256;
				// Grab the pixel
				int rgb = s.getpixel(texX % s.width, texY % s.height);
				// Draw it
				appletgraphics.setColor(new Color(rgb));
				appletgraphics.fillRect(x + imgx, y + imgy, 1, 1);
			}
		}
	}
	
	// Take the vbuffer and draw it to the specified column.
	public void rendervbuffer(Color[] vbuffer, int column) {
		int pos = 0;
		for (Color i : vbuffer) {
			appletgraphics.setColor(i);
			appletgraphics.fillRect(column, pos, 1, 1);
			pos++; 
		}
	}
	
	// Take the relevant floor, wall, and ceiling image data and push it all into a vertical color buffer.
	public void fillvbuffer(Color[] vbuffer, String walltype, int whichwall, int wallpos, int wallheight, int topofwall, int bottomofwall, boolean useimage, float dist, int wallx, int wally) {
		ImageData wallimg = null;
		if (useimage) {
			if (maptowalldefinitions.containsKey(walltype)) {
				wallimg = maptowalldefinitions.get(walltype);
			}
			else {
				wallimg = maptowalldefinitions.get("1");
			}
		}
		
		int castangle = appletptr.engine.fetchraydata();
		
		for (int p = 0; p < angles.PROJECTIONPLANEHEIGHT; p++) {
    		// This pixel is the ceiling color
    		if (p < (topofwall + player.getHeight())) {
    			// If we're using images this will be filled later.
    			if (!useimagesceiling) {
    			vbuffer[p] = new Color(ceilingcolor[0], ceilingcolor[1], ceilingcolor[2]);
    			}
    		}
    		// This pixel is on a wall
    		else if (((p >= (topofwall + player.getHeight())) && (p <= (bottomofwall + player.getHeight())))) {
    			// Draw using images
    			if (useimage) {
    				// Image data is an array of integers, picture it like a 2-dimensional array
    				// wallpos = the x coordinate of the image, where the ray hit the wall
    				// So all we have to do is loop through the y coordinates on the image pixels
    				
    				// 256 and 128 factors to avoid float calculations
    			    int d = ((p - player.getHeight()) * 256) - (this.height * 128) + (wallheight * 128);
    			    int texY = ((d * 64) / wallheight) / 256;
    			    int rgb = wallimg.getpixel(wallpos, texY % wallimg.height);
    			    
	    			if (whichwall == 0) {
	    				vbuffer[p] = new Color(rgb);
	    				if (variablelightlevels) {
		    		        int r = vbuffer[p].getRed();
		    		        int b = vbuffer[p].getBlue();
		    		        int g = vbuffer[p].getGreen();
		    		        Cell[] gamemap = this.map.getMap();
		    		        int mod = (gamemap[(wallx) + ((wally) * this.map.getMapx())]).lightlevelmod;
		    		        if (mod > 0) {
		    		        	r = (r + mod) < 255 ? (r + mod) : 255;
		    		        	g = (g + mod) < 255 ? (g + mod) : 255;
		    		        	b = (b + mod) < 255 ? (b + mod) : 255;
		    		        	vbuffer[p] = new Color(r, g, b);
		    		        }
		    		        else if (mod < 0) {
		    		        	r = (r + mod) > 0 ? (r + mod) : 0;
		    		        	g = (g + mod) > 0 ? (g + mod) : 0;
		    		        	b = (b + mod) > 0 ? (b + mod) : 0;
		    		        	vbuffer[p] = new Color(r, g, b);
		    		        }
	    		        }
	    			}
	    			
	    			else {
	    				// Now lighten the wall.
	    				Color tempcolor = new Color(rgb);
	    				int red = tempcolor.getRed() + 10;
	    				int green = tempcolor.getGreen() + 10;
	    				int blue = tempcolor.getBlue() + 10;
	    				if (red > 255) red = 255;
	    				if (green > 255) green = 255;
	    				if (blue > 255) blue = 255;
	    				vbuffer[p] = new Color(red, green, blue);
	    				if (variablelightlevels) {
		    		        int r = vbuffer[p].getRed();
		    		        int b = vbuffer[p].getBlue();
		    		        int g = vbuffer[p].getGreen();
		    		        Cell[] gamemap = this.map.getMap();
		    		        int mod = (gamemap[(wallx) + ((wally) * this.map.getMapx())]).lightlevelmod;
		    		        if (mod > 0) {
		    		        	r = (r + mod) < 255 ? (r + mod) : 255;
		    		        	g = (g + mod) < 255 ? (g + mod) : 255;
		    		        	b = (b + mod) < 255 ? (b + mod) : 255;
		    		        	vbuffer[p] = new Color(r, g, b);
		    		        }
		    		        else if (mod < 0) {
		    		        	r = (r + mod) > 0 ? (r + mod) : 0;
		    		        	g = (g + mod) > 0 ? (g + mod) : 0;
		    		        	b = (b + mod) > 0 ? (b + mod) : 0;
		    		        	vbuffer[p] = new Color(r, g, b);
		    		        }
	    		        }
	    			}
	    		}
    			// No images
    			else {
    				// Horizontal wall
    				if (whichwall == 0) {
    					vbuffer[p] = new Color(64, 64, 64);
    				}
    				else {
    					vbuffer[p] = new Color(128, 128, 128);
    				}
    			}
    		}
    		// Fun fact: The data for floor and ceiling is symmetrical around the middle of the screen.
    		// So only half the calculations are needed!
    		else if (p > bottomofwall) {
    			if (useimagesfloor || useimagesceiling) {
    				/*
    				 * 
    				 * FLOORCASATING IDEA
    				Cast from the middle of the wall to the bottom, just like normal

    				90 degrees = this
    				for (int c = PROJECTIONPLANEHEIGHT/2; c < PROJECTIONPLANEHEIGHT; c += 5) {
    					get distance tan(degrees) * dist to wall
    					split into x and y components

    					floor pixel cell = (playerx + x component) / TILE_SIZE
    					floor texture pixel = (playerx + x component) % IMAGE_WIDTH

    					same for y

    				}
    				 */
    				
    				int floorrayangle = angles.ANGLE90 - (angles.PROJECTIONPLANEHEIGHT - (p / 2));
    		        
    		        float newdist = appletptr.engine.tantable[floorrayangle] * dist;
    		        
    		        int newx = Math.abs(player.getX() + ((int) (appletptr.engine.costable[castangle] * newdist)));
    		        int newy = Math.abs(player.getY() + ((int) (appletptr.engine.sintable[castangle] * newdist)));
    		        
    		        if (useimagesfloor) {
	    		        // Position of pixel on textures
	    		        int floorTexX = ((newx % map.TILE_SIZE) % floorimg.width);
	    		        int floorTexY = ((newy % map.TILE_SIZE) % floorimg.height);
	    		        // Floor Pixel
	    		        //vbuffer[p] = new Color((floorimg.data[floorTexX][floorTexY] >> 1) & 8355711);
	    		        try {
	    		        	vbuffer[p] = new Color((floorimg.data[floorTexX][floorTexY]));
	    		        }
	    		        // FIXME: Floor texture cell out of array bounds
	    		        catch (Exception e) {
	    		        	e.printStackTrace();
	    		        	vbuffer[p] = new Color(floorcolor[0], floorcolor[1], floorcolor[2]);
	    		        }
	    		        if (variablelightfloors) {
		    		        int r = vbuffer[p].getRed();
		    		        int b = vbuffer[p].getBlue();
		    		        int g = vbuffer[p].getGreen();
		    		        Cell[] gamemap = this.map.getMap();
		    		        int mod;
		    		        try {
		    		        	mod = (gamemap[(newx / this.map.TILE_SIZE) + ((newy / this.map.TILE_SIZE) * this.map.getMapx())]).lightlevelmod;
		    		        }
		    		        catch (Exception e) {
		    		        	//e.printStackTrace();
		    		        	mod = 0;
		    		        }
		    		        if (mod > 0) {
		    		        	r = (r + mod) < 255 ? (r + mod) : 255;
		    		        	g = (g + mod) < 255 ? (g + mod) : 255;
		    		        	b = (b + mod) < 255 ? (b + mod) : 255;
		    		        	vbuffer[p] = new Color(r, g, b);
		    		        }
		    		        else if (mod < 0) {
		    		        	r = (r + mod) > 0 ? (r + mod) : 0;
		    		        	g = (g + mod) > 0 ? (g + mod) : 0;
		    		        	b = (b + mod) > 0 ? (b + mod) : 0;
		    		        	vbuffer[p] = new Color(r, g, b);
		    		        }
	    		        }
    		        }
    		        if (useimagesceiling) {
	    		        // Ceiling image may be of different dimensions than floor image, so don't use the same vars.
	    		        int ceilingTexX = (newx % ceilingimg.width);
	    		        int ceilingTexY = (newy % ceilingimg.height);
	    		        // Ceiling Pixel
	    		        try {
	    		        	vbuffer[angles.PROJECTIONPLANEHEIGHT - p] = new Color((ceilingimg.data[ceilingTexX][ceilingTexY]  >> 1) & 8355711);
	    		        }
	    		        catch (Exception e) {
	    		        	//e.printStackTrace();
	    		        	vbuffer[angles.PROJECTIONPLANEHEIGHT - p] = new Color(ceilingcolor[0], ceilingcolor[1], ceilingcolor[2]);
	    		        }
	    		        if (variablelightceilings) {
		    		        int r = vbuffer[angles.PROJECTIONPLANEHEIGHT - p].getRed();
		    		        int b = vbuffer[angles.PROJECTIONPLANEHEIGHT - p].getBlue();
		    		        int g = vbuffer[angles.PROJECTIONPLANEHEIGHT - p].getGreen();
		    		        Cell[] gamemap = this.map.getMap();
		    		        int mod;
		    		        try {
		    		        	mod = (gamemap[(newx / this.map.TILE_SIZE) + ((newy / this.map.TILE_SIZE) * this.map.getMapx())]).lightlevelmod;
		    		        }
		    		        catch (Exception e) {
		    		        	continue;
		    		        	//e.printStackTrace();
		    		        	//mod = 0;
		    		        }
		    		        if (mod > 0) {
		    		        	r = (r + mod) < 255 ? (r + mod) : 255;
		    		        	g = (g + mod) < 255 ? (g + mod) : 255;
		    		        	b = (b + mod) < 255 ? (b + mod) : 255;
		    		        	vbuffer[angles.PROJECTIONPLANEHEIGHT - p] = new Color(r, g, b);
		    		        }
		    		        else if (mod < 0) {
		    		        	r = (r + mod) > 0 ? (r + mod) : 0;
		    		        	g = (g + mod) > 0 ? (g + mod) : 0;
		    		        	b = (b + mod) > 0 ? (b + mod) : 0;
		    		        	vbuffer[angles.PROJECTIONPLANEHEIGHT - p] = new Color(r, g, b);
		    		        }
	    		        }
    		        }
    			}
    			else {
    				vbuffer[p] = new Color(floorcolor[0], floorcolor[1], floorcolor[2]);
    			}
    		}
    	}
	}
	
	// Fetch the Graphics we draw to.
	public Graphics getGraphics() {
		return this.appletgraphics;
	}
	
	// Called by Raycaster.castRay(), draw a vertical column of wall.
	public void drawwallslice(String walltype, int wallx, int wally, int whichwall, int wallpos, float dist, int castcolumn, int distancetotheprojectionplane, List<Integer> cells) {
		Color[] vbuffer = new Color[angles.PROJECTIONPLANEHEIGHT];
    	int topofwall;
    	int bottomofwall;

    	// Compensate for the "fishbowl effect"
    	dist /= fishbowleffecttable[castcolumn];
    	// Calculate the wall's height and where to start drawing it.
    	int wallheight = (int)(WALL_HEIGHT * (float)distancetotheprojectionplane / dist);
    	bottomofwall = (angles.PROJECTIONPLANEHEIGHT / 2) + (int)(wallheight * 0.5F);
    	topofwall = angles.PROJECTIONPLANEHEIGHT - bottomofwall;
    	
    	if (bottomofwall >= angles.PROJECTIONPLANEHEIGHT) {
            bottomofwall = angles.PROJECTIONPLANEHEIGHT - 1; }

    	// Fill the vertical buffer
    	fillvbuffer(vbuffer, walltype, whichwall, wallpos, wallheight, topofwall, bottomofwall, useimages, dist, wallx, wally);
    	
    	for (int i = 0; i < 5; i++) {
        	// Then draw it
    		rendervbuffer(vbuffer, castcolumn + i);
    	}
	}

}
